summaryrefslogtreecommitdiff
path: root/app/api/auth/[...nextauth]/route.ts
diff options
context:
space:
mode:
Diffstat (limited to 'app/api/auth/[...nextauth]/route.ts')
-rw-r--r--app/api/auth/[...nextauth]/route.ts258
1 files changed, 177 insertions, 81 deletions
diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts
index f5d49f77..2b168746 100644
--- a/app/api/auth/[...nextauth]/route.ts
+++ b/app/api/auth/[...nextauth]/route.ts
@@ -1,5 +1,4 @@
-// Updated NextAuth configuration with dynamic session timeout from database
-
+// auth/config.ts - 업데이트된 NextAuth 설정
import NextAuth, {
NextAuthOptions,
Session,
@@ -9,15 +8,18 @@ import NextAuth, {
import { JWT } from "next-auth/jwt"
import CredentialsProvider from 'next-auth/providers/credentials'
import { SAMLProvider } from './saml/provider'
-import { getUserById } from '@/lib/users/repository'
+import { getUserByEmail, getUserById } from '@/lib/users/repository'
import { authenticateWithSGips, verifyExternalCredentials } from '@/lib/users/auth/verifyCredentails'
import { verifyOtpTemp } from '@/lib/users/verifyOtp'
import { getSecuritySettings } from '@/lib/password-policy/service'
+import { verifySmsToken } from '@/lib/users/auth/passwordUtil'
+import { SessionRepository } from '@/lib/users/session/repository'
+import { loginSessions } from '@/db/schema'
// 인증 방식 타입 정의
type AuthMethod = 'otp' | 'email' | 'sgips' | 'saml'
-// 모듈 보강 선언 (인증 방식 추가)
+// 모듈 보강 선언 (기존과 동일)
declare module "next-auth" {
interface Session {
user: {
@@ -30,7 +32,8 @@ declare module "next-auth" {
domain?: string | null
reAuthTime?: number | null
authMethod?: AuthMethod
- sessionExpiredAt?: number | null // 세션 만료 시간 추가
+ sessionExpiredAt?: number | null
+ dbSessionId?: string | null // DB 세션 ID 추가
}
}
@@ -42,6 +45,7 @@ declare module "next-auth" {
domain?: string | null
reAuthTime?: number | null
authMethod?: AuthMethod
+ dbSessionId?: string | null
}
}
@@ -54,11 +58,12 @@ declare module "next-auth/jwt" {
domain?: string | null
reAuthTime?: number | null
authMethod?: AuthMethod
- sessionExpiredAt?: number | null // 세션 만료 시간 추가
+ sessionExpiredAt?: number | null
+ dbSessionId?: string | null
}
}
-// 보안 설정 캐시 (성능 최적화)
+// 보안 설정 캐시 (기존과 동일)
let securitySettingsCache: {
data: any | null
lastFetch: number
@@ -69,7 +74,6 @@ let securitySettingsCache: {
ttl: 5 * 60 * 1000 // 5분 캐시
}
-// 보안 설정을 가져오는 함수 (캐시 적용)
async function getCachedSecuritySettings() {
const now = Date.now()
@@ -80,7 +84,6 @@ async function getCachedSecuritySettings() {
securitySettingsCache.lastFetch = now
} catch (error) {
console.error('Failed to fetch security settings:', error)
- // 기본값 사용
securitySettingsCache.data = {
sessionTimeoutMinutes: 480 // 8시간 기본값
}
@@ -90,11 +93,28 @@ async function getCachedSecuritySettings() {
return securitySettingsCache.data
}
+// 클라이언트 IP 추출 헬퍼
+function getClientIP(req: any): string {
+ const forwarded = req.headers['x-forwarded-for']
+ const realIP = req.headers['x-real-ip']
+
+ if (forwarded) {
+ return forwarded.split(',')[0].trim()
+ }
+
+ if (realIP) {
+ return realIP
+ }
+
+ return req.ip || req.connection?.remoteAddress || '127.0.0.1'
+}
+
export const authOptions: NextAuthOptions = {
providers: [
- // OTP provider
+ // OTP 로그인 (기존 유지)
CredentialsProvider({
- name: 'Credentials',
+ id: 'credentials-otp',
+ name: 'OTP',
credentials: {
email: { label: 'Email', type: 'text' },
code: { label: 'OTP code', type: 'text' },
@@ -107,9 +127,7 @@ export const authOptions: NextAuthOptions = {
return null
}
- // 보안 설정에서 세션 타임아웃 가져오기
const securitySettings = await getCachedSecuritySettings()
- const sessionTimeoutMs = securitySettings.sessionTimeoutMinutes * 60 * 1000
const reAuthTime = Date.now()
return {
@@ -125,61 +143,101 @@ export const authOptions: NextAuthOptions = {
}
},
}),
-
- // ID/패스워드 provider (S-Gips와 일반 이메일 구분)
+
+ // MFA 완료 후 최종 인증 (DB 연동 버전)
CredentialsProvider({
- id: 'credentials-password',
- name: 'Username Password',
+ id: 'credentials-mfa',
+ name: 'MFA Verification',
credentials: {
- username: { label: "Username", type: "text" },
- password: { label: "Password", type: "password" },
- provider: { label: "Provider", type: "text" },
+ userId: { label: 'User ID', type: 'text' },
+ smsToken: { label: 'SMS Token', type: 'text' },
+ tempAuthKey: { label: 'Temp Auth Key', type: 'text' },
},
async authorize(credentials, req) {
- if (!credentials?.username || !credentials?.password) {
- return null;
+ if (!credentials?.userId || !credentials?.smsToken || !credentials?.tempAuthKey) {
+ console.error('MFA credentials missing')
+ return null
}
-
+
try {
- let authResult;
- const isSSgips = credentials.provider === 'sgips';
-
- if (isSSgips) {
- authResult = await authenticateWithSGips(
- credentials.username,
- credentials.password
- );
- } else {
- authResult = await verifyExternalCredentials(
- credentials.username,
- credentials.password
- );
+ // DB에서 임시 인증 정보 확인
+ const tempAuth = await SessionRepository.getTempAuthSession(credentials.tempAuthKey)
+ if (!tempAuth || tempAuth.userId !== credentials.userId) {
+ console.error('Temp auth expired or not found')
+ return null
}
-
- if (authResult.success && authResult.user) {
- return {
- id: authResult.user.id,
- name: authResult.user.name,
- email: authResult.user.email,
- imageUrl: authResult.user.imageUrl ?? null,
- companyId: authResult.user.companyId,
- techCompanyId: authResult.user.techCompanyId,
- domain: authResult.user.domain,
- reAuthTime: Date.now(),
- authMethod: isSSgips ? 'sgips' as AuthMethod : 'email' as AuthMethod,
- };
+
+ // SMS 토큰 검증
+ const smsVerificationResult = await verifySmsToken(Number(credentials.userId), credentials.smsToken)
+ if (!smsVerificationResult || !smsVerificationResult.success) {
+ console.error('SMS token verification failed')
+ return null
}
- return null;
+ // 사용자 정보 조회
+ const user = await getUserById(Number(credentials.userId))
+ if (!user) {
+ console.error('User not found after MFA verification')
+ return null
+ }
+
+ // 임시 인증 정보를 사용됨으로 표시
+ await SessionRepository.markTempAuthSessionAsUsed(credentials.tempAuthKey)
+
+ // 보안 설정 및 세션 정보 설정
+ const securitySettings = await getCachedSecuritySettings()
+ const reAuthTime = Date.now()
+ const sessionExpiredAt = new Date(reAuthTime + (securitySettings.sessionTimeoutMinutes * 60 * 1000))
+
+ // DB에 로그인 세션 생성
+ const ipAddress = getClientIP(req)
+ const userAgent = req.headers?.['user-agent']
+ const dbSession = await SessionRepository.createLoginSession({
+ userId: String(user.id),
+ ipAddress,
+ userAgent,
+ authMethod: tempAuth.authMethod,
+ sessionExpiredAt,
+ })
+
+ console.log(`MFA completed for user ${user.email} (${tempAuth.authMethod})`)
+
+ return {
+ id: String(user.id),
+ email: user.email,
+ imageUrl: user.imageUrl ?? null,
+ name: user.name,
+ companyId: user.companyId,
+ techCompanyId: user.techCompanyId as number | undefined,
+ domain: user.domain,
+ reAuthTime,
+ authMethod: tempAuth.authMethod as AuthMethod,
+ dbSessionId: dbSession.id,
+ }
+
} catch (error) {
- console.error("Authentication error:", error);
- return null;
+ console.error('MFA authorization error:', error)
+ return null
}
+ },
+ }),
+
+ // 1차 인증용 프로바이더 (기존 유지)
+ CredentialsProvider({
+ id: 'credentials-first-auth',
+ name: 'First Factor Authentication',
+ credentials: {
+ username: { label: "Username", type: "text" },
+ password: { label: "Password", type: "password" },
+ provider: { label: "Provider", type: "text" },
+ },
+ async authorize(credentials, req) {
+ return null
}
}),
- // SAML Provider
+ // SAML Provider (기존 유지)
SAMLProvider({
id: "credentials-saml",
name: "SAML SSO",
@@ -199,18 +257,15 @@ export const authOptions: NextAuthOptions = {
session: {
strategy: 'jwt',
- // JWT 기본 maxAge는 30일로 설정하되, 실제 세션 만료는 콜백에서 처리
maxAge: 30 * 24 * 60 * 60, // 30일
},
callbacks: {
- // JWT 콜백 - 세션 타임아웃 설정 (만료 체크는 session 콜백에서)
async jwt({ token, user, account, trigger, session }) {
- // 보안 설정 가져오기
const securitySettings = await getCachedSecuritySettings()
const sessionTimeoutMs = securitySettings.sessionTimeoutMinutes * 60 * 1000
- // 최초 로그인 시
+ // 최초 로그인 시 (MFA 완료 후)
if (user) {
const reAuthTime = Date.now()
token.id = user.id
@@ -223,34 +278,44 @@ export const authOptions: NextAuthOptions = {
token.reAuthTime = reAuthTime
token.authMethod = user.authMethod
token.sessionExpiredAt = reAuthTime + sessionTimeoutMs
+ token.dbSessionId = user.dbSessionId
}
- // 인증 방식 결정 (account 정보 기반)
- if (account && !token.authMethod) {
+ // SAML 인증 시 DB 세션 생성
+ if (account && account.provider === 'credentials-saml' && token.id) {
const reAuthTime = Date.now()
- if (account.provider === 'credentials-saml') {
+ const sessionExpiredAt = new Date(reAuthTime + sessionTimeoutMs)
+
+ try {
+ const dbSession = await SessionRepository.createLoginSession({
+ userId: token.id,
+ ipAddress: '0.0.0.0', // SAML의 경우 IP 추적 제한적
+ authMethod: 'saml',
+ sessionExpiredAt,
+ })
+
token.authMethod = 'saml'
token.reAuthTime = reAuthTime
token.sessionExpiredAt = reAuthTime + sessionTimeoutMs
- } else if (account.provider === 'credentials') {
- // OTP는 이미 user.authMethod에서 설정됨
- if (!token.sessionExpiredAt) {
- token.sessionExpiredAt = (token.reAuthTime || Date.now()) + sessionTimeoutMs
- }
- } else if (account.provider === 'credentials-password') {
- // credentials-password는 이미 user.authMethod에서 설정됨
- if (!token.sessionExpiredAt) {
- token.sessionExpiredAt = (token.reAuthTime || Date.now()) + sessionTimeoutMs
- }
+ token.dbSessionId = dbSession.id
+ } catch (error) {
+ console.error('Failed to create SAML session:', error)
}
}
- // 세션 업데이트 시 (재인증 시간 업데이트)
+ // 세션 업데이트 시
if (trigger === "update" && session) {
if (session.reAuthTime !== undefined) {
token.reAuthTime = session.reAuthTime
- // 재인증 시간 업데이트 시 세션 만료 시간도 연장
token.sessionExpiredAt = session.reAuthTime + sessionTimeoutMs
+
+ // DB 세션 업데이트
+ if (token.dbSessionId) {
+ await SessionRepository.updateLoginSession(token.dbSessionId, {
+ lastActivityAt: new Date(),
+ sessionExpiredAt: new Date(session.reAuthTime + sessionTimeoutMs)
+ })
+ }
}
if (session.user) {
@@ -263,14 +328,18 @@ export const authOptions: NextAuthOptions = {
return token
},
- // Session 콜백 - 세션 만료 체크 및 정보 포함
async session({ session, token }: { session: Session; token: JWT }) {
// 세션 만료 체크
if (token.sessionExpiredAt && Date.now() > token.sessionExpiredAt) {
console.log(`Session expired for user ${token.email}. Expired at: ${new Date(token.sessionExpiredAt)}`)
- // 만료된 세션 처리 - 빈 세션 반환하여 로그아웃 유도
+
+ // DB 세션 만료 처리
+ if (token.dbSessionId) {
+ await SessionRepository.logoutSession(token.dbSessionId)
+ }
+
return {
- expires: new Date(0).toISOString(), // 즉시 만료
+ expires: new Date(0).toISOString(),
user: null as any
}
}
@@ -287,12 +356,12 @@ export const authOptions: NextAuthOptions = {
reAuthTime: token.reAuthTime as number | null,
authMethod: token.authMethod as AuthMethod,
sessionExpiredAt: token.sessionExpiredAt as number | null,
+ dbSessionId: token.dbSessionId as string | null,
}
}
return session
},
- // Redirect 콜백
async redirect({ url, baseUrl }) {
if (url.startsWith("/")) {
return `${baseUrl}${url}`;
@@ -309,18 +378,45 @@ export const authOptions: NextAuthOptions = {
error: '/auth/error',
},
- // 디버깅을 위한 이벤트 로깅
events: {
async signIn({ user, account, profile }) {
const securitySettings = await getCachedSecuritySettings()
console.log(`User ${user.email} signed in via ${account?.provider} (authMethod: ${user.authMethod}), session timeout: ${securitySettings.sessionTimeoutMinutes} minutes`);
+
+ // 이미 MFA에서 DB 세션이 생성된 경우가 아니라면 여기서 생성
+ if (account?.provider !== 'credentials-mfa' && user.id) {
+ try {
+ // 기존 활성 세션 확인
+ const existingSession = await SessionRepository.getActiveSessionByUserId(user.id)
+ if (!existingSession) {
+ const sessionExpiredAt = new Date(Date.now() + (securitySettings.sessionTimeoutMinutes * 60 * 1000))
+
+ await SessionRepository.createLoginSession({
+ userId: user.id,
+ ipAddress: '0.0.0.0', // signIn 이벤트에서는 IP 접근 제한적
+ authMethod: user.authMethod || 'unknown',
+ sessionExpiredAt,
+ })
+ }
+ } catch (error) {
+ console.error('Failed to create session in signIn event:', error)
+ }
+ }
},
+
async signOut({ session, token }) {
console.log(`User ${session?.user?.email || token?.email} signed out`);
+
+ // DB에서 세션 로그아웃 처리
+ const userId = session?.user?.id || token?.id
+ const dbSessionId = session?.user?.dbSessionId || token?.dbSessionId
+
+ if (dbSessionId) {
+ await SessionRepository.logoutSession(dbSessionId)
+ } else if (userId) {
+ // dbSessionId가 없는 경우 사용자의 모든 활성 세션 로그아웃
+ await SessionRepository.logoutAllUserSessions(userId)
+ }
}
}
}
-
-const handler = NextAuth(authOptions)
-export { handler as GET, handler as POST }
-